package hydrap2p.proxy.manager;

import hydrap2p.ServiceLocation;
import hydrap2p.helper.Pinger;
import hydrap2p.helper.ThriftServerThread;
import hydrap2p.logging.Level;
import hydrap2p.servicemanager.ServiceManager;
import hydrap2p.servicemanager.NameTaken;
import hydrap2p.servicemanager.ServiceListener;

import java.io.IOException;
import java.net.BindException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;

public class Server implements ProxyManager.Iface, ServiceListener.Iface {

	private static Server instance;
	
	public static void connectToManager(int peerid, ServiceLocation remotemanager) throws IOException
	{
		new ConnectToManager(peerid, remotemanager).start();
	}
	
	private static class ConnectToManager extends Thread
	{
		public int peerid;
		public ServiceLocation remotemanager;
		public ConnectToManager(int peerid, ServiceLocation remotemanager) {
			this.peerid = peerid;
			this.remotemanager = remotemanager;
			setName("ConnectToManager: "+peerid+"@"+remotemanager.address+":"+remotemanager.port);
		}
		@Override public void run()
		{
			try {
				instance.connectToManagerReal(peerid, remotemanager);
			} catch (IOException e) {
				// Failed to connect to manager
			}
		}
	}
	
	public static boolean isRunning()
	{
		return instance!=null;
	}
	
	public static void stop()
	{
		instance.stopReal();
	}
	
	private void stopReal()
	{

		if (managerthread!=null)
			managerthread.shutdown();
		if (listenerthread!=null)
			listenerthread.shutdown();
		System.exit(0);
	}
	
	private ThriftServerThreadBind managerthread;
	private ThriftServerThread listenerthread;

	private int managerpeerid;
	private ServiceLocation realmanagerlocation;
	private ServiceLocation remotelistenerlocation;
	private ServiceManager.Iface realmanager;
	private Map<String, ServiceLocation> serviceports;
	private Map<String, ServiceLocation> proxyserviceports;
	private ServiceListeners listeners;
	
	public void connectToManagerReal(int peerid, ServiceLocation remotemanager) throws IOException
	{
		managerpeerid=peerid;
		try {
			ProxyLogging.log(Level.DEBUG, "ProxyManager - Server", "Connecting to Remote Manager: "+remotemanager.address+":"+remotemanager.port);
			realmanagerlocation=ServiceTunnels.getInstance().createConnection(managerpeerid, remotemanager);
			remotelistenerlocation=CallbackManager.getInstance().openCallbackPort(listenerthread.getLocation(), null, peerid);
			
			//ProxyLogging.log(Level.DEBUG, "ProxyManager - Server", "Initiating connection to local virtual port: "+realmanagerlocation.address+":"+realmanagerlocation.port);
			TTransport socket=new TSocket(realmanagerlocation.address,realmanagerlocation.port);
			socket.open();
			realmanager=new ServiceManager.Client(new TBinaryProtocol(socket));
			realmanager.subscribe(remotelistenerlocation);
		}
		catch (Exception e)
		{
			e.printStackTrace();
			ProxyLogging.log(Level.ERROR, "ProxyManager - Server", "Failed to connect to remote manager: "+e.getLocalizedMessage());
			// Failure to connect to remote manager
			throw new IOException("Failed to connect to remote manager");
		}
	}
	
	public Server(int port) throws IOException
	{
		instance=this;

		serviceports=new TreeMap<String, ServiceLocation>();
		proxyserviceports=new TreeMap<String, ServiceLocation>();
		listeners=new ServiceListeners();

		ProxyLogging.log(Level.INFO, "ProxyManager - Server", "Started to run Server at port "+port);

		try {
			listenerthread=new ThriftServerThread(new ServiceListener.Processor(this), null);
			managerthread=new ThriftServerThreadBind(new ProxyManager.Processor(this), "ProxyManager", port);
		} catch (BindException e) {
			throw e;
		} catch (IOException e) {
			throw e;
		} catch (NameTaken e) {
			// Shouldn't happen
		}
		synchronized(serviceports)
		{
			proxyserviceports.put("ProxyManager", managerthread.getLocation());
		}
	}
	
	public void start()
	{
		managerthread.start();
		listenerthread.start();
	}
	
	@Override
	public ServiceLocation getRealServiceLocation(String arg0)
			throws TException {
		synchronized(serviceports)
		{
			if (serviceports.containsKey(arg0))
				return serviceports.get(arg0);
		}
		return null;
	}

	@Override
	public ServiceLocation openCallbackPort(ServiceLocation arg0, ServiceLocation arg1)
			throws TException {
		return CallbackManager.getInstance().openCallbackPort(arg0, arg1, 0);
	}

	@Override
	public void setProxyServiceLocation(String arg0, ServiceLocation arg1)
			throws NameTaken, TException {
		synchronized(serviceports)
		{
			if (proxyserviceports.containsKey(arg0))
				throw new NameTaken(arg0, arg1);
			new MyServicePinger(arg0, arg1);
			proxyserviceports.put(arg0, arg1);
		}
	}

// Manager methods
	@Override
	public void setServiceLocation(String arg0, ServiceLocation arg1)
			throws NameTaken, TException {
		ServiceLocation remote=CallbackManager.getInstance().openCallbackPort(arg1, realmanagerlocation, 0);
		realmanager.setServiceLocation(arg0, remote);
	}

	@Override
	public ServiceLocation getServiceLocation(String arg0) throws TException {
		synchronized(serviceports)
		{
			if (proxyserviceports.containsKey(arg0))
				return proxyserviceports.get(arg0);
			if (serviceports.containsKey(arg0))
				return serviceports.get(arg0);
			StringBuilder total=new StringBuilder("[");
			for (String name:serviceports.keySet())
			{
				total.append(name+", ");
			}
			total.delete(total.length()-2, total.length());
			total.append("]");
			ProxyLogging.log(Level.MISHAP, "Server.getServiceLocation", "Could not find "+arg0+" in list of remote services "+total);
		}
		return null;
	}
	
	private Map<String, ServiceLocation> getActiveServiceLocations()
	{
		Map<String, ServiceLocation> ret = new TreeMap<String, ServiceLocation>();
		synchronized(serviceports)
		{
			ret.putAll(serviceports);
			ret.putAll(proxyserviceports);
		}
		return ret;
	}
	
	@Override
	public Map<String, ServiceLocation> getAllServiceLocations()
			throws TException {
		return getActiveServiceLocations();
	}
	
	@Override
	public void delServiceLocation(String arg0) throws TException {
		realmanager.delServiceLocation(arg0);
	}

	@Override
	public boolean subscribe(ServiceLocation arg0) throws TException {
		return listeners.addListener(arg0, getActiveServiceLocations());
	}

	@Override
	public void unsubscribe(ServiceLocation arg0) throws TException {
		listeners.removeListener(arg0);
	}
	
// Listener methods
	@Override
	public void newService(String arg0, ServiceLocation arg1) throws TException {
		ServiceLocation destination;
		synchronized(serviceports)
		{
			ServiceLocation previous=CallbackManager.getInstance().getLocalPort(arg1);
			if (previous!=null)
			{	// if we own the remote port, because it's a callback
				// save the local side of the callback in the list
				destination=previous;
				ProxyLogging.log(Level.INFO, "Server.newService", "Found existing callback port at "+previous);
			}
			else
			{
				try {
					destination = ServiceTunnels.getInstance().createConnection(managerpeerid, arg1);
				} catch (IOException e) {
					// Could not create connection
					throw new TException("Failed to connect to remote port");
				}
			}
			serviceports.put(arg0, destination);
		}
		listeners.newService(arg0, destination);
	}

	@Override
	public void delService(String arg0) throws TException {
		synchronized(serviceports)
		{
			ServiceLocation virtual=serviceports.get(arg0);
			if (virtual!=null)
			{
				ServiceTunnels.getInstance().removeTunnel(virtual.port);
				CallbackManager.getInstance().closeCallback(virtual);
				serviceports.remove(arg0);
			}
		}
		listeners.delService(arg0);
	}

// Service methods
	@Override
	public void ping() throws TException {
	}

	@Override
	public List<String> supportedInterfaces() throws TException {
		return Arrays.asList("Service", "ServiceManager", "ProxyManager", "ServiceListener");
	}

	private class MyServicePinger extends Pinger
	{		
		public MyServicePinger(String name, ServiceLocation location) {
			super(name, location);
		}

		@Override
		public void failure(String name) {
			synchronized(proxyserviceports)
			{
				proxyserviceports.remove(name);
			}
		}
	}
}
